# 尤雨溪推荐 的 ni 神器
# 环境准备
git clone https://github.com/antfu/ni.git
# package.json
"bin": {
"ni": "bin/ni.js",
"nci": "bin/nci.js",
"nr": "bin/nr.js",
"nu": "bin/nu.js",
"nx": "bin/nx.js",
"nrm": "bin/nrm.js"
},
"scripts": {
"prepublishOnly": "rimraf dist && npm run build",
"watch": "npm run build -- --watch",
"?ni": "npm install",
"ni": "esno src/ni.ts",
"nci": "esno src/nci.ts",
"nr": "esno src/nr.ts",
"nu": "esno src/nu.ts",
"nx": "esno src/nx.ts",
"nrm": "esno src/nrm.ts",
"dev": "esno src/ni.ts",
"build": "rimraf dist && tsup src/ni.ts src/nci.ts src/nr.ts src/nu.ts src/nx.ts src/nrm.ts src/index.ts --format cjs,esm --dts",
"release": "npx git-ensure && npx bumpp --commit --push --tag",
"lint": "eslint \"**/*.ts\"",
"lint:fix": "npm run lint -- --fix",
"test": "c8 ava"
},
"devDependencies": {
"@antfu/eslint-config-ts": "^0.7.0",
"@types/ini": "^1.3.30",
"@types/node": "^16.7.10",
"@types/prompts": "^2.4.0",
"ava": "^3.15.0",
"c8": "^7.8.0",
"esbuild-register": "^3.0.0",
"eslint": "^7.32.0",
"esm": "^3.2.25",
"esno": "^0.9.1",
"execa": "^5.1.1",
"find-up": "^5.0.0",
"ini": "^2.0.0",
"prompts": "^2.4.1",
"rimraf": "^3.0.2",
"terminal-link": "^3.0.0",
"tsup": "^4.14.0",
"typescript": "^4.4.2"
},
# 一些三方依赖库
- execa: child_process 的增强版,对命令执行更加人性化
- find-up: 通过遍历父目录查找文件或目录
- prompts: ini 配置文件的写入和读取
# bin命令
"ni": "bin/ni.js"
- install"nci": "bin/nci.js"
- clean install"nr": "bin/nr.js"
- run"nu": "bin/nu.js"
- upgrade"nx": "bin/nx.js"
- execute"nrm": "bin/nrm.js
- remove
这里可以看出,当在命令行中敲出命令的时候,背后运行的js文件,注意这个项目使用 ts 进行编写的, 所有存在一个 build 命令
"build": "rimraf dist && tsup src/ni.ts src/nci.ts src/nr.ts src/nu.ts src/nx.ts src/nrm.ts src/index.ts --format cjs,esm --dts"
rimraf: node版本 的
rm -rf
tsup: 无需配置便可以 使用
ts
,基础配置由 esbuild 提供,--dts
可以生成 d.ts 声明文件
# ni & nu & nx & nrm
// ni.ts
import { parseNi } from './commands'
// nu.ts
import { parseNu } from './commands'
// nx.ts
import { parseNx } from './commands'
// nrm.ts
import { parseNrm } from './commands'
import { runCli } from './runner'
runCli(parseNi)
# nci.ts
import { parseNi } from './commands'
import { runCli } from './runner'
runCli(
(agent, _, hasLock) => parseNi(agent, ['--frozen-if-present'], hasLock),
{ autoInstall: true },
)
# 总结
从现在已有代码来看,有两个核心的功能
- commands 中 parseNx,parseNu,parseNrm,parseNi 等函数
- runCli
# runner.ts
runCLi所在的代码文件
import { resolve } from 'path'
import prompts from 'prompts'
import execa from 'execa'
import { Agent, agents } from './agents'
import { getDefaultAgent, getGlobalAgent } from './config'
import { detect, DetectOptions } from './detect'
import { remove } from './utils'
const DEBUG_SIGN = '?'
export interface RunnerContext {
hasLock?: boolean
cwd?: string
}
export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<string | undefined> | string | undefined
export async function runCli(fn: Runner, options: DetectOptions = {}) {
// process.argv 第一个元素 执行路径,第二个元素 执行js的文件路径
// filter 中 真值 进行过滤
const args = process.argv.slice(2).filter(Boolean)
try {
// 尝试使用 run 调用函数
await run(fn, args, options)
}
catch (error) {
process.exit(1)
}
}
// run 主体函数
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
// debug 参数
const debug = args.includes(DEBUG_SIGN)
if (debug)
remove(args, DEBUG_SIGN)
// process.cwd() 返回 Node.js 进程的当前工作目录
let cwd = process.cwd()
let command
if (args[0] === '-C') {
// 将路径或路径片段的序列解析为绝对路径
cwd = resolve(cwd, args[1])
// 删除 0 1 参数
args.splice(0, 2)
}
// 是否为全局
const isGlobal = args.includes('-g')
if (isGlobal) {
// 默认配置 全局代理 为 npm
command = await fn(getGlobalAgent(), args)
}
else {
// 借助 detect 使用 lock 文件 拿到使用的 包管理器,并可能会全局安装所需包管理器
let agent = await detect({ ...options, cwd }) || getDefaultAgent()
if (agent === 'prompt') {
// agent 手动选择 包管理器
agent = (await prompts({
name: 'agent',
type: 'select',
message: 'Choose the agent',
choices: agents.map(value => ({ title: value, value })),
})).agent
if (!agent)
return
}
// 生成命令
command = await fn(agent as Agent, args, {
hasLock: Boolean(agent),
cwd,
})
}
// 命令为空 直接放回
if (!command)
return
// 命令为空 直接放回 DEBUG 状态 打印命令不执行
if (debug) {
// eslint-disable-next-line no-console
console.log(command)
return
}
// 借助 execa 库 来执行命令
await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}
# commands
这里提供了几个转义命令的功能函数
function getCommand(
agent: Agent,
command: Command,
args: string[] = [],
) {
if (!(agent in AGENTS))
throw new Error(`Unsupported agent "${agent}"`)
// 从常量文件中 得到命令字符串
const c = AGENTS[agent][command]
if (typeof c === 'function')
return c(args)
if (!c)
throw new Error(`Command "${command}" is not support by agent "${agent}"`)
// 替换 留出的文件
return c.replace('{0}', args.join(' ')).trim()
}
# parseNi
const parseNi = <Runner>((agent, args, ctx) => {
if (args.length === 1 && args[0] === '-v') {
// eslint-disable-next-line no-console
console.log(`@antfu/ni v${version}`)
process.exit(0)
}
if (args.length === 0)
return getCommand(agent, 'install')
if (args.includes('-g'))
// exclude 从数组中删除某个字符串
return getCommand(agent, 'global', exclude(args, '-g'))
if (args.length === 1 && args[0] === '-f')
return getCommand(agent, 'install', args)
if (args.includes('--frozen-if-present')) {
args = exclude(args, '--frozen-if-present')
return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
}
if (args.includes('--frozen'))
return getCommand(agent, 'frozen', exclude(args, '--frozen'))
return getCommand(agent, 'add', args)
})
# parseNr
const parseNr = <Runner>((agent, args) => {
if (args.length === 0)
args.push('start')
if (args.includes('--if-present')) {
args = exclude(args, '--if-present')
args[0] = `--if-present ${args[0]}`
}
return getCommand(agent, 'run', args)
})
# parseNu
const parseNu = <Runner>((agent, args) => {
if (args.includes('-i'))
return getCommand(agent, 'upgrade-interactive', exclude(args, '-i'))
return getCommand(agent, 'upgrade', args)
})
# parseNrm
const parseNrm = <Runner>((agent, args) => {
if (args.includes('-g'))
return getCommand(agent, 'global_uninstall', exclude(args, '-g'))
return getCommand(agent, 'uninstall', args)
})
# parseNx
const parseNx = <Runner>((agent, args) => {
return getCommand(agent, 'execute', args)
})
# 工具类函数
# exclude
function remove<T>(arr: T[], v: T) {
const index = arr.indexOf(v)
if (index >= 0)
arr.splice(index, 1)
return arr
}
// 将传入的数据 从数组中删除(为浅拷贝,对原数组不产生影响)
function exclude<T>(arr: T[], v: T) {
return remove(arr.slice(), v)
}
# detect
async function detect({ autoInstall, cwd }: DetectOptions) {
// 通过 找寻 locks 文件 来判断使用了哪一个包管理器
const result = await findUp(Object.keys(LOCKS), { cwd })
const agent = (result ? LOCKS[path.basename(result)] : null)
if (agent && !cmdExists(agent)) {
if (!autoInstall) {
console.warn(`Detected ${agent} but it doesn't seem to be installed.\n`)
if (process.env.CI)
process.exit(1)
const link = terminalLink(agent, INSTALL_PAGE[agent])
const { tryInstall } = await prompts({
name: 'tryInstall',
type: 'confirm',
message: `Would you like to globally install ${link}?`,
})
if (!tryInstall)
process.exit(1)
}
await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
}
return agent
}
# 总结
这个包主要做了以下件事:
- 找到项目的包管理器,如果没有找到,就让用户选择一个包管理器
- 将命令进行转移成对应的包管理器命令,并执行。
← 第十一期 玩具vite 第十三期 open →